package edu.drexel.jrexel2d;

import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

import edu.drexel.jrexel2d.camera.Camera;
import edu.drexel.jrexel2d.collision.Collision;
import edu.drexel.jrexel2d.collision.Detector;
import edu.drexel.jrexel2d.gamestate.Level;
import edu.drexel.jrexel2d.input.CharacterActions;
import edu.drexel.jrexel2d.input.GameAction;
import edu.drexel.jrexel2d.input.GameInput;
import edu.drexel.jrexel2d.input.InputEngine;
import edu.drexel.jrexel2d.input.InputMap;
import edu.drexel.jrexel2d.networking.GamePacket;
import edu.drexel.jrexel2d.networking.NetworkClient;
import edu.drexel.jrexel2d.networking.Serializer;
import edu.drexel.jrexel2d.networking.sync.HandShake;
import edu.drexel.jrexel2d.networking.sync.PlayerHistory;
import edu.drexel.jrexel2d.networking.sync.ServerGameUpdate;
import edu.drexel.jrexel2d.objects2D.Player;
import edu.drexel.jrexel2d.objects2D.Point2D;
import edu.drexel.jrexel2d.physics.PhysicsEngine;
import edu.drexel.jrexel2d.rendering.RenderingEngine;

public class jrexel2d_client {
	/** position of quad */
	float x = 400, y = 300;
	/** angle of quad rotation */
	float rotation = 0;

	private final int displayWidth = 800;
	private final int displayHeight = 600;

	/** time at last frame */
	private long lastFrame;
	private long lastAnim; // Last time animation was done
	private long time;
	private long lastTime;

	/** frames per second */
	private int fps;
	/** last fps time */
	private long lastFPS;

	private int delta;

	private long nextGameTick;

	private final int gameHertz = 60;
	private final long skipTicks = 1000 / gameHertz;
	private final int frameSkip = 1;

	private RenderingEngine renderingEngine;
	private Level curLevel;
	private Level serverUpdatedLevel;
	private LinkedList<PlayerHistory> playerHistory;

	private ArrayList<Collision> cols;

	private PhysicsEngine physicsEngine;

	private final boolean isNetworkedClient;
	private NetworkClient networkClient;

	private final InputMap inputMap;

	private Camera camera;
	private final int playerId;
	private InputEngine inputEngine;

	public jrexel2d_client(InputMap inputMap, boolean isNetworkedClient,
			int playerId) {
		this.isNetworkedClient = isNetworkedClient;
		this.inputMap = inputMap;
		System.out.println("Networked client = " + isNetworkedClient);
		this.physicsEngine = new PhysicsEngine();
		this.playerId = playerId;
		camera = new Camera();
		renderingEngine = new RenderingEngine();
		inputEngine = new InputEngine();

		physicsEngine = new PhysicsEngine();
		if (isNetworkedClient) {
			playerHistory = new LinkedList<PlayerHistory>();

		}

	}

	public void start() {

		try {
			Display.setDisplayMode(new DisplayMode(displayWidth, displayHeight));
			Display.setInitialBackground(.5f, .5f, .5f);
			Display.create();
		} catch (LWJGLException e) {
			e.printStackTrace();
			System.exit(0);
		}

		renderingEngine.initGL(); // init OpenGL

		nextGameTick = getTime();
		updateDelta(); // call once before loop to initialize lastFrame
		lastFPS = getTime(); // call before loop to initialize fps timer

		lastAnim = getTime();
		time = getTime();

		if (isNetworkedClient) {
			initializeClientAndHandshakeWithServer();
			try {
				networkClient.getGameSocket().getSocket().setSoTimeout(3);
			} catch (SocketException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}
		curLevel = new Level("level1");

		physicsEngine.init(curLevel);
		curLevel.setObjectPhysicsEngine(physicsEngine);

		gameLoop();

		Display.destroy();
	}

	public void gameLoop() {
		int loopCount;
		GameInput input = new GameInput();
		delta = updateDelta();
		int numSentNetworkedUpdates = 0;
		while (!Display.isCloseRequested()) {
			loopCount = 0;
			time = getTime();
			while (time > nextGameTick && loopCount < frameSkip) {
				if (isNetworkedClient) {// && input.getGameActions().size() > 0){

					// Client Side prediction
					// update client first
					// updateGameWithLocalInput(input, delta);

					// then send updates to server
					ServerGameUpdate serverGameUpdate = updateNetworkModule(input);
					if (serverGameUpdate != null
							&& serverGameUpdate.getUpdatedObjects() != null) {

						// Sync the local player's position with the position
						// stored on the server.
						if (serverGameUpdate.getTimeOfPlayerUpdates().size() > 0
								&& serverGameUpdate.getTimeOfPlayerUpdates()
										.containsKey(playerId)) {
							syncClientAndServer(serverGameUpdate);

						} else {
							if (!(playerHistory.size() > 0)) {
								if (serverGameUpdate.getTimeOfPlayerUpdates()
										.containsKey(playerId)) {
									curLevel.mergeObjectsAndPlayersExceptCurrentPlayer(
											serverGameUpdate
													.getUpdatedObjects(),
											playerId);
								} else {
									curLevel.mergeObjectsAndPlayers(serverGameUpdate
											.getUpdatedObjects());
								}

								curLevel.getBackdrop().setLocation(
										serverGameUpdate.getBackdropLocation());
							}
						}

					}

				}

				loopCount++;
				nextGameTick = nextGameTick + skipTicks;
				delta = updateDelta();
				// input = getUserInput();

				input = getUserInput();
				updateGameWithLocalInput(input, delta);
				updateCollisionModule();

				updatePhysicsModule(delta);
				if (!isNetworkedClient)
					inputEngine.clearGameActions(inputMap,
							input.getGameActions());

				updateFPS(); // update FPS Counter
				// delta = updateDelta();
				// System.out.println("Elapsed: "+ delta);
				lastTime = time;
				//time = getTime();
			}

			render();

			updateDisplay();
		}

	}

	private void syncClientAndServer(ServerGameUpdate serverGameUpdate) {
		Map<Integer, Long> timeOfPlayerUpdatesFromServer = serverGameUpdate
				.getTimeOfPlayerUpdates();
		Long timeOfLastPlayerUpdatedReceivedByServer = 0L;

		if (playerHistory.size() > 0
				&& timeOfPlayerUpdatesFromServer.size() > 0) {
			timeOfLastPlayerUpdatedReceivedByServer = timeOfPlayerUpdatesFromServer
					.get(playerId);

			// Remove all game actions that happened before the latest recieved by the server.
			List<PlayerHistory> playerHistorysToRemove = new ArrayList<PlayerHistory>();
			boolean reachedCurrentHistory = false;
			for (PlayerHistory ph : playerHistory) {
				if (ph.getTime() == timeOfLastPlayerUpdatedReceivedByServer) { //currentPlayerHistory = ph;
					playerHistorysToRemove.add(ph);
					reachedCurrentHistory = true;
				} else if (!reachedCurrentHistory) {
					playerHistorysToRemove.add(ph);
				}
			}
			playerHistory.removeAll(playerHistorysToRemove);

			int localPlayersObjectId = curLevel.getPlayerByPlayerId(playerId)
					.getId();
			// get players latest position from the server
			Point2D localPlayersPositionFromServer = serverGameUpdate
					.getPlayerByObjectId(localPlayersObjectId);

			// set the local players position back to the position that the
			// server
			// has for it
			// then apply the game
			Player localPlayer = curLevel
					.getPlayerByObjectId(localPlayersObjectId);

			CharacterActions preserveLastAction = localPlayer.getLastAction();

			localPlayer.setLocation(localPlayersPositionFromServer);

			//update backdrop
			curLevel.getBackdrop().setLocation(
					serverGameUpdate.getBackdropLocation());

			//update all other objects:
			curLevel.mergeObjectsAndPlayersExceptCurrentPlayer(
					serverGameUpdate.getUpdatedObjects(), playerId);

			// apply all game actions to the latest player position recieved
			// from
			// the server.
			for (PlayerHistory ph : playerHistory) {
				for (GameAction ga : ph.getGameActions()) {
					if (ga.getCharacterId() == localPlayer.getPlayerId()) {
						inputEngine.executeGameAction(ga, localPlayer);

						updateCollisionModule();
						updatePhysicsModule(delta);
						camera.updateCamera(curLevel, displayWidth,
								displayHeight);
					}
				}
			}

			localPlayer.setLastAction(preserveLastAction);

		}
	}

	public void updateGameWithLocalInput(GameInput input, int delta) {

		List<Player> players = curLevel.getPlayers();
		List<GameAction> gameActions = input.getGameActions();
		for (GameAction ga : gameActions) {
			for (Player player : players) {
				if (ga.getCharacterId() == player.getPlayerId()) {
					ga.setTime(time);
					inputEngine.executeGameAction(ga, player);

					/*					updateCollisionModule();

										updatePhysicsModule(delta);*/

					camera.updateCamera(curLevel, displayWidth, displayHeight);
				}

			}
		}

		// keep track of local players previous game actions.
		if (input.getGameActions().size() > 0) {
			if (isNetworkedClient) {
				playerHistory.addLast(new PlayerHistory(curLevel
						.getPlayerByPlayerId(playerId), time, input
						.getGameActions()));
			}
		}
	}

	private void initializeClientAndHandshakeWithServer() {
		HandShake handShake = new HandShake();

		if (isNetworkedClient) {
			try {

				// ServerMasterSocket
				networkClient = new NetworkClient(
						InetAddress.getByName("localhost"), 1835, 1024);
				networkClient.sendSerializable(handShake);
				// GamePacket packet = networkClient.receivePacket();
				networkClient.receivePacket();
				GamePacket packet = networkClient.getPacket();
				byte[] data = packet.getData();
				// System.out.println("Recieved GameInput from Server");
				handShake = (HandShake) Serializer.deserializeBytes(data);

				networkClient.getGameSocket().setPort(handShake.getPort());
			} catch (SocketException | UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}
	}

	public GameInput getUserInput() {
		for (Player p : curLevel.getPlayers()) {
			p.clearActions();
		}
		GameInput gameInput = inputMap.getUserInput();
		return gameInput;

	}

	public ServerGameUpdate updateNetworkModule(GameInput input) {
		networkClient.sendSerializable(input);
		if (input.getGameActions().size() > 0)
			inputEngine.clearGameActions(inputMap, input.getGameActions());
		if (networkClient.receivePacket() == false) {
			return null;
		}

		byte[] data = networkClient.getData();
		// System.out.println("Recieved GameInput from Server");
		return (ServerGameUpdate) Serializer.deserializeBytes(data);
	}

	public void updateCollisionModule() {
		Detector d = new Detector(curLevel);

		cols = d.check();
	}

	public void updatePhysicsModule(int delta) {

		physicsEngine.gameLoopUpdates(cols, delta);

	}

	public void render() {

		renderingEngine.clear();
		Boolean anim = false;
		if ((getTime() - lastAnim) > 200) {
			anim = true;
			lastAnim = getTime();
		}
		/*
		 * if((getTime() - test) > 2000) { // This is a test
		 * curLevel.spawnPlayer() ; test = getTime() ; }
		 */

		renderingEngine.renderLevel(curLevel, anim);

	}

	public void updateDisplay() {
		Display.update();
	}

	/**
	 * Calculate how many milliseconds have passed since last frame.
	 * 
	 * @return milliseconds passed since last frame
	 */
	public int updateDelta() {

		long time = getTime();
		int delta = (int) (time - lastFrame);

		lastFrame = time;

		return delta;
	}

	private int getDelta() {
		long time = getTime();
		int delta = (int) (time - lastFrame);

		return delta;
	}

	/**
	 * Get the accurate system time
	 * 
	 * @return The system time in milliseconds
	 */
	public long getTime() {
		long time = (Sys.getTime() * 1000) / Sys.getTimerResolution();
		// System.out.println("Current Time: "+ time);
		return time;
	}

	/**
	 * Calculate the FPS and set it in the title bar
	 */
	public void updateFPS() {

		if (getTime() - lastFPS > 1000) {
			Display.setTitle("FPS: " + fps);
			fps = 0;
			lastFPS += 1000;
		}

		fps++;
	}

}